package csc411;

import java.io.*;
import java.net.*;

public class Server {

	private int port = 9876; // default port
	private AccountsManager accountsManager;
	private RequestsManager requestsManager;
	private ObserversManager observersManager;
	private DatagramSocket serverSocket;
	// Semantic invocation
	//      0 = at-least-once (default)
	//      1 = at-most-once
	private int semanticInvocation = 0;
	private double packetLossRate = 0.8; // 50% chance of packet loss
	private int timeout = 1000; // timeout for server response in millisecond

	public Server(String[] args) {

		// get commandline args
		for (int i = 0; i < args.length; i = i + 2) {

			// get server port
			if (args[i].equals("-p")) {
				this.port = Integer.parseInt(args[i + 1]);
			}

			// get semantic invocation type
			if (args[i].equals("-s")) {
				this.semanticInvocation = Integer.parseInt(args[i + 1]);
			}

			// get packet loss rate
			if (args[i].equals("-r")) {
				this.packetLossRate = Double.parseDouble(args[i + 1]);
			}

			// get timeout
			if (args[i].equals("-t")) {
				this.timeout = Integer.parseInt(args[i + 1]);
			}
		}

		// start server socket
		try {

			this.serverSocket = new DatagramSocket(this.port);

		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// start accounts manager
		this.accountsManager = new AccountsManager();

		// requests manager
		this.requestsManager = new RequestsManager();

		// observers manager
		this.observersManager = new ObserversManager();

                // print server settings
                System.out.println("[SERVER SETTINGS]");
                System.out.println("-----------------");
                System.out.println("Server Port: " + this.port);
                System.out.print("Semantic Invocation: ");
                if (this.semanticInvocation == 0) {
                        System.out.println("At-least-once");
                }
                if (this.semanticInvocation == 1) {
                        System.out.println("At-most-once");
                }
		System.out.println("Packet loss rate: " + this.packetLossRate + "%");
                System.out.println();
        }

	public static void main(String args[]) throws Exception {

		Server server = new Server(args);

		System.out.println();
		server.accountsManager.printAllAccounts();

		//System.out.println();
		//server.requestsManager.printRequestList();

		//System.out.println();
		//server.observersManager.printObserversList();

		byte[] receiveData = new byte[1024];

		while (true) {
			System.out.println("[SERVER] Started listening for requests...");

			String response;

			// start listening for incoming packets (blocking receive)
			DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
			server.serverSocket.receive(receivePacket);

			// get client's IP address and port
			InetAddress clientAddress = receivePacket.getAddress();
			int clientPort = receivePacket.getPort();

			// dispatch message
			try {

				// dispatch message and receive response from method
				response = server.dispatcher(clientAddress, clientPort, receiveData);

				// send server response back to client
				server.sendResponse(clientAddress, clientPort, response);

			} catch (Exception e) {
				System.out.println(e.getMessage());
			}

		}
	}

	public String dispatcher(InetAddress clientAddress, int clientPort, byte[] data)
			throws Exception {

		String response = "";
		String callbackMessage = "Default callback message";

		int methodId = -1;
		int requestId = -1;

		// unpack message
		Message message = new Message(data);
		message.unpack();

		// get method id
		methodId = message.methodId;

		// get request id
		requestId = message.requestId;


		System.out.println("[CLIENT] " + clientAddress.getHostAddress() + ":" + clientPort + " " + Utils.getTime());
		System.out.println("[CLIENT] Request Id: " + requestId + " / Method Id: " + methodId);

		// if semantic invocation is at-most-once,
		// check if request is a duplicate and already executed,
		//              return stored request response.
		// if request is a duplicate but server still servicing the request,
		//              do nothing
		// if request is new,
		//              call method (service the request)

		// ------------------------------------------------------------------
		// -    Duplicate?              -       Method executed?        -       Action                  -
		// ------------------------------------------------------------------
		// -            No                      -               No                              -       Execute                 -
		// -            No                      -               Yes                             -       Not possible    -
		// -            Yes                     -               No                              -       Do nothing              -
		// -            Yes                     -               Yes                             -       Retrieve stored message -
		// ------------------------------------------------------------------

		if (this.semanticInvocation == 1) {     // at-most-once

			if (this.requestsManager.isDuplicateRequest(clientAddress, clientPort, requestId)) {

				// if it is a duplicate request, get the stored request
				Request storedRequest = this.requestsManager.getRequest(clientAddress, clientPort, requestId);

				// check if the server has completed servicing the request
				if (storedRequest.hasCompletedExecution()) {

					System.out.println("[SERVER] Duplicated request (" + requestId + ") from " + clientAddress.getHostAddress() + ":" + clientPort);

					// return stored response as string
					return storedRequest.getResponse();

				} else {

					// tell server not to do anything because still servicing request
					// note: this is unlikely if (time to execute a method) < (timeout period)
					throw new Exception("[SERVER] Still servicing request (" + requestId + ") from " + clientAddress.getHostAddress() + ":" + clientPort);

				}

			}
		}

		// else if we reach here,
		//              either: semantic invocation is at-least-once or
		//                              semantic invocation is at-most-once and it is a new request

		// store unexecuted request in the Requests Manager
		this.requestsManager.storeNewRequest(clientAddress, clientPort, requestId);

		// based on the method id,
		// the dispatcher will call the appropriate stub
		if (methodId == 1) {
			// call open account
			response = this.openAccount_stub(message);
			callbackMessage = "New account request for (" + message.name + ")";
			callbackMessage += ": " + response;


		} else if (methodId == 2) { // Nadiah (220210:1725)
			// call close account
			response = this.closeAccount_stub(message);

			callbackMessage = "Close account request for (" + message.accountNum + ")";
			callbackMessage += ": " + response;

		} else if (methodId == 3) {
			// call deposit/withdraw stub
			response = this.depositWithdraw_stub(message);

			// prepare callback message
			if (message.amount > 0) {
				callbackMessage = "Deposit request to account (" + message.accountNum + ")";
				callbackMessage += ": " + response;
			} else if (message.amount < 0) {
				callbackMessage = "Withdrawal request from account (" + message.accountNum + ")";
				callbackMessage += ": " + response;
			}
		} else if (methodId == 5) {  //Riz 250310
			// check balance
			response = this.checkBalance(message);

			callbackMessage = "Check balance for account (" + message.accountNum + ")";
			callbackMessage += ": " + response;
		} else if (methodId == 6) { // Nadiah (220310:2050)
			// call transfer account
			response = this.transfer_stub(message);

			callbackMessage = "Transfer request from (" + message.accountNum + ")"
					+ "to (" + message.accountNumToTransfer + ")";
			callbackMessage += ": " + response;

		} else if (methodId == 7) {
			// call client registration stub
			response = this.registerClient_stub(clientAddress, clientPort, message);

			callbackMessage = "Client registration request from (" + clientAddress.getHostAddress() + ":" + clientPort + ") ";
			callbackMessage += ": " + response;

		} else {
			response = "Unknown method called.";
		}

		// updated the stored request after completed execution
		this.requestsManager.updateRequest(clientAddress, clientPort, requestId, response);


		// send callback message to observers
		if (methodId == 7) {
			//System.out.println("Don't send callback");
		} else {
			System.out.println("[SERVER] Sending callback...");
			this.observersManager.callback(callbackMessage);
		}

		return response;
	}

	public void sendResponse(InetAddress clientAddress, int clientPort, String response) {

		byte[] sendData = new byte[1024];

		boolean isPacketLost = false;

		// prepare server response back to client
		sendData = response.getBytes();

		// prepare send packet
		DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, clientAddress, clientPort);

		// do a coin flip to simulate loss of server response
		isPacketLost = Utils.isDroppedPacket(packetLossRate);

		// send server response if its not supposed to be lost
		if (!isPacketLost) {

			try {

				// send server response
				System.out.println("[SERVER] Response sent");
				this.serverSocket.send(sendPacket);

			} catch (IOException e) {
				System.out.println(e.toString());
			}

		} else {

			System.out.println("[SERVER] Server response dropped");

		}
		System.out.println();

	}

	// TODO 150310
	public String openAccount_stub(Message message) {
		System.out.println("[SERVER] Open Account request for (" + message.name + ")");

		//Riz updated 250310

		String response;

		// call accountsManager.addNewAccount() which returns the new account object
		// ensure your server response follows the assignment requirements

		try {

			Account account = accountsManager.addNewAccount(message.name,
					message.password, message.type, message.amount);

			// prepare success response
			response = "Successfully opened an account for Account #: " + account.getAccountNum() + ". Initial Balance: " + account.getBalanceString();

		} catch (Exception e) {
			// we reach here if the given accountnum/name/password is wrong
			response = e.getMessage();
		}

		System.out.println("[SERVER] Response: " + response);
		this.accountsManager.printAllAccounts();
		return response;
	}

	// Nadiah (160310:1610)
	public String closeAccount_stub(Message message) {
		System.out.println("[SERVER] Close Account request for account (" + message.accountNum + ")");

		String response;

		// call accountsManager.removeAccount() which returns the new account object
		// ensure your server response follows the assignment requirements

		try {

			Account account = accountsManager.removeAccount(message.accountNum,
					message.name, message.password);

			// prepare success response
			response = "Successfully closed an account for (" + account.getAccountNum() + ")";

		} catch (Exception e) {
			// we reach here if the given accountnum/name/password is wrong
			response = e.getMessage();
		}

		System.out.println("[SERVER] Response: " + response);
		return response;
	}

	public String depositWithdraw_stub(Message message) {
		System.out.println("[SERVER] Deposit/Withdraw request for account (" + message.accountNum + ")");

		String response;

		// if amount > 0 then its a deposit operation,
		// else if amount < 0 then its a withdraw operation

		if (message.amount > 0) {

			// remember to catch exception because accountsManager throws exception
			// if the given account num/name/password is wrong
			try {

				Account account = accountsManager.deposit(message.accountNum,
						message.name, message.password,
						message.type, message.amount);

				// prepare success response
				response = "Successfully deposited (" + Utils.printMoney(message.type, message.amount) + "). "
						+ "New balance: " + account.getBalanceString();

			} catch (Exception e) {
				// we reach here if the given accountnum/name/password is wrong
				response = e.getMessage();
			}


		} else if (message.amount < 0) {

			// remember to catch exception because accountsManager throws exception
			// if the given account num/name/password is wrong
			try {

				Account account = accountsManager.withdraw(message.accountNum,
						message.name, message.password,
						message.type, message.amount);

				// prepare success response
				response = "Successfully withdrew (" + Utils.printMoney(message.type, message.amount * -1) + "). "
						+ "New balance: " + account.getBalanceString();

			} catch (Exception e) {
				// we reach here if the given accountnum/name/password is wrong
				response = e.getMessage();
			}

		} else {
			// if amount == 0, which is unlikely
			response = "Amount cannot be zero";
		}

		System.out.println("[SERVER] Response: " + response);
		return response;
	}

	//Riz 250310
	public String checkBalance(Message message) {
		System.out.println("Check Balance Stub called!");

		String response;

		// call accountsManager.getAccount() which returns the new account object
		// ensure your server response follows the assignment requirements

		try {

			Account account = accountsManager.getAccount(message.accountNum,
					message.name, message.password);

			// prepare success response
			response = "The current balance for Account (" + account.getAccountNum() + ")"
					+" is " + account.getBalanceString();

		} catch (Exception e) {
			// we reach here if the given accountnum/name/password is wrong
			response = e.getMessage();
		}

		System.out.println("Server response: " + response);
		System.out.println();
		return response;
	}

	// Nadiah (220310:2200)
	public String transfer_stub(Message message) {
		System.out.println("[SERVER] Transfer request from account (" + message.accountNum + ") to account (" + message.accountNumToTransfer + ")");

		String response;

		if (message.accountNum != message.accountNumToTransfer) {
			try {

				// verify that both accounts exist first before performing withdraw/transfer
				Account target = accountsManager.getAccountTransfer(message.accountNumToTransfer);
				Account source = accountsManager.getAccount(message.accountNum, message.name, message.password);

				Account account = accountsManager.withdraw(message.accountNum,
						message.name, message.password,
						message.type, message.amount * (-1)); // withdraw amount must be in negative form.

				Account accountTransfer = accountsManager.depositTransfer(message.accountNumToTransfer,
						message.type, message.amount);

				// prepare success response
				response = "Successfully transferred (" + Utils.printMoney(message.type, message.amount) + ") "
						+ "from account (" + account.getAccountNum() + ") to account ("
						+ accountTransfer.getAccountNum() + "). "
						+ "New balance: " + account.getBalanceString();

			} catch (Exception e) {
				// we reach here if the given accountnum/name/password is wrong
				response = e.getMessage();
			}
		} else {
			response = "Can't perform a transfer to the same account.";
		}

		System.out.println("[SERVER] Response: " + response);
		return response;
	}

	// FEEZ 160310
	public String registerClient_stub(InetAddress clientAddress, int clientPort, Message message) {
		System.out.println("[SERVER] Client Registration request for (" + clientAddress.getHostAddress() + ":" + clientPort + ")");

		String response;

		// do server operations here
		this.observersManager.register(clientAddress, clientPort, message.interval);

		response = "OK";

		System.out.println("[SERVER] Response: " + response);
		return response;
	}
}
